/*
 * Copyright (C) 2012 Damien Girard <dgirard@nativesoft.fr>
 * Copyright (C) 2012 Sebastien Baudrand <sebbaudrand@gmail.com>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package fr.umlv.qroxy.communication.transfer;

import fr.umlv.qroxy.cache.CacheWriter;
import fr.umlv.qroxy.http.HttpCodes;
import fr.umlv.qroxy.http.HttpException;
import fr.umlv.qroxy.rules.Rule;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.logging.Logger;

/**
 * Chunked transfer operation.
 *
 * To use when the Content-length is not known and transfer mode is chunked.
 *
 * @author Damien Girard <dgirard@nativesoft.fr>
 * @author Sebastien Baudrand <sebbaudrand@gmail.com>
 */
public class ChunkedTransfer implements Transfer {

    private int headerEndPosition;
    private ChunkHeader chunkHeader = new ChunkHeader(-1, -1);
    private long totalRead = 0;
    private long totalWrite = 0;
    private long chunkRead = 0;
    private long chunkWrite = 0;
    private boolean firstRead = true;
    private CacheWriter cacheWriter;

    /**
     * Chunked Transfer.
     *
     * @param headerEndPosition End position of the HTTP header in the buffer.
     */
    public ChunkedTransfer(int headerEndPosition) {
        this.headerEndPosition = headerEndPosition;
        this.cacheWriter = null;
    }

    /**
     * Chunked Transfer.
     *
     * @param headerEndPosition End position of the HTTP header in the buffer.
     */
    public ChunkedTransfer(int headerEndPosition, CacheWriter cacheWriter) {
        this.headerEndPosition = headerEndPosition;
        this.cacheWriter = cacheWriter;
    }

    @Override
    public TransferResult read(SocketChannel channel, ByteBuffer buffer, boolean useOnlyBuffer) throws IOException, HttpException {
        // Reading datas.
        int nbRead = 0;
        if (!useOnlyBuffer) {
            nbRead = channel.read(buffer);
            if (nbRead < 0) {
                // If nbRead returns -1 but the buffer have data,
                // this means that maybe we have already read all the data of this chunked transfer
                // but not treated the last chunk.
                if (buffer.position() == 0) {
                    return new TransferResult(nbRead, false);
                }
            } else {
                totalRead += nbRead;
                chunkRead += nbRead;
            }
        }

        // Do I have to read a chunk header ?
        if (chunkHeader.chunkLength < 0) {
            // Saving the buffer position.
            int bufferPosition = buffer.position();
            buffer.position(headerEndPosition);

            // Trying to read the header.
            ChunkHeader newChunkHeader;
            try {
                newChunkHeader = readChunkHeader(buffer, bufferPosition, !firstRead);
            } catch (Exception e) {
                // Fatal error.
                throw new HttpException(HttpCodes.HTTP_BAD_GATEWAY, "Chunked read error. " + e.getMessage());
            }
            if (newChunkHeader.chunkLength >= 0) {
                // Valid chunk.
                chunkHeader = newChunkHeader;
                chunkHeader.dataLength += headerEndPosition;
                firstRead = false;
                headerEndPosition = 0; // The chunk header will ever be at zero. (The write operation do that)
            }
            System.out.println(">>>>>>> " + chunkHeader.chunkLength + " --- Data: " + chunkHeader.dataLength);
            buffer.position(bufferPosition);
        }

        return new TransferResult(nbRead, (chunkHeader.chunkLength != -1));
    }

    @Override
    public TransferResult write(SocketChannel channel, ByteBuffer buffer, int limit) throws IOException {
        // Sanity check.
        if (chunkHeader.chunkLength == -1) {
            throw new RuntimeException("Transfer: Write operation requested without a valid chunkSize. YOU MUST NOT CALL THIS METHOD IS THERE IS NO VALID CHUNKSIZE.");
        }
        if (chunkWrite > chunkHeader.dataLength) {
            throw new RuntimeException("Transfer: All data were already write. YOU MUST NOT CALL THIS METHOD IF EVERYTHING IS WRITTEN.");
        }

        // Storing buffer informations.
        int bufferLimit = buffer.limit();
        int bufferPosition = buffer.position();

        // Rewinding the buffer and setting the limits to the
        // amount of data that I have to write.
        buffer.position(0);

        if (chunkHeader.chunkLength == 0) {
            // Special case for the chunkSize of zero, I have to write the footers data.
            buffer.limit(chunkHeader.dataLength);
        } else {

            if (chunkWrite + bufferPosition > chunkHeader.dataLength) {
                buffer.limit((int) (chunkHeader.dataLength - chunkWrite)); // I have to write only a part of the buffer.
            } else {
                buffer.limit(bufferPosition); // I have to write the entire buffer.
            }
        }
        
        // Handling the limit (for QoS).
        if (limit > 0 && buffer.limit() > limit) {
            buffer.limit(limit);
        }

        int nbWrite = channel.write(buffer);
        if (nbWrite < 0) {
            // Restauring buffer state.
            buffer.limit(bufferLimit);
            buffer.position(bufferPosition);

            return new TransferResult(nbWrite, false);
        }

        // Writing data wrote from the network to the cache.
        if (cacheWriter != null) {
            if (chunkHeader.dataLength - chunkHeader.chunkLength < nbWrite) {
                // Storing position and limit.
                int storedLimit = buffer.limit();
                int storedPosition = buffer.position();
                
                buffer.limit(buffer.position());
                buffer.position(chunkHeader.dataLength - chunkHeader.chunkLength); // Removing http header and chunk headers.
                try {
                    cacheWriter.write(buffer);
                } catch (IOException e) {
                    Logger.getLogger(CacheWriter.class.getName()).info("Error while writing data to cache file"+ e);
                    cacheWriter.close(true);
                    cacheWriter = null;
                }
                
                buffer.limit(storedLimit);
                buffer.position(storedPosition);
            }
        }

        // Data wrote.
        totalWrite += nbWrite;
        chunkWrite += nbWrite;

        // Compacting the buffer and reseting everything.
        buffer.limit(bufferPosition);
        buffer.compact();

        buffer.limit(bufferLimit);
        buffer.position(bufferPosition - nbWrite);

        // Do I have to read a new chunk header?
        if (chunkHeader.chunkLength == 0) {
            chunkHeader = new ChunkHeader(-1, -1);
            chunkRead = 0;
            chunkWrite = 0;

            return new TransferResult(nbWrite, true);
        } else if (chunkWrite == chunkHeader.dataLength) {
            chunkHeader = new ChunkHeader(-1, -1);
            chunkRead = 0;
            chunkWrite = 0;

            return new TransferResult(nbWrite, false, false); // Forcing a read operation to read the new chunk header.
        }

        return new TransferResult(nbWrite, false, (buffer.position() != 0));
    }

    /**
     * Internal class to create a new chunked header
     */
    private class ChunkHeader {

        private int chunkLength; // Parsed chunk length.
        private int dataLength; // Contains the number of bytes to write.

        public ChunkHeader(int chunkLength, int dataLength) {
            this.chunkLength = chunkLength;
            this.dataLength = dataLength;
        }
    }

    /**
     * Returns the chunk header size.
     *
     * @param buffer Buffer where to read the chunk header.
     * @param limit Limit of the reading operation.
     * @param readCRLF Set to true if there is a crlf before the chunk header.
     *
     * The buffer position is set after to the first data of the chunk.
     *
     * @return -1 if header is incomplete.
     */
    private ChunkHeader readChunkHeader(ByteBuffer buffer, int limit, boolean readCRLF) throws Exception {
        // Parsing the chunk header size.
        // Note: Chunk extension are ignored.
        int bufferOriginalPosition = buffer.position();

        StringBuilder hexLength = new StringBuilder();
        byte lastRead = 0;
        boolean ignore = false;
        boolean parsed = false;
        while (buffer.position() <= limit && parsed == false) {
            byte b = buffer.get();
            //System.out.print((char) b);
            switch (b) {
                case ';':
                    ignore = true;
                    break;
                case '\r':
                    lastRead = b;
                    break;
                case '\n':
                    if (lastRead == '\r') {
                        if (readCRLF) { // Do I have to read an empty CRLF?
                            readCRLF = false;
                        } else {
                            parsed = true;
                        }
                        break;
                    } else {
                        throw new Exception("Malformed chunk header.");
                    }
                default:
                    if (!ignore) {
                        hexLength.append((char) b);
                    }
                    break;
            }
        }
        if (parsed == false) {
            // Chunk header not read.
            // Probably it is missing a part. (half header read)
            return new ChunkHeader(-1, -1);
        }

        // Chunk header parsed. Trying to convert it to int.
        int chunkLength = -1;
        try {
            chunkLength = Integer.parseInt(hexLength.toString(), 16);
        } catch (NumberFormatException ex) {
            throw new Exception("Chunk header invalid. " + ex.toString());
        }

        // If 0 is read, I must read a CRLF after then, and all the footers.
        if (chunkLength == 0) {
            /*
             * if (buffer.position() + 2 < limit) { // Missing end CRLF or
             * footer. return new ChunkHeader(-1, -1); } else { if (buffer.get()
             * != '\r') { return new ChunkHeader(-1, -1); } if (buffer.get() !=
             * '\n') { return new ChunkHeader(-1, -1); } }
             */
            // Reading data until reaching '\r\n\r\n'
            // All thoses footers data are ignored.
            byte lastByte = 0;
            boolean foundCrLf = true;
            while (buffer.position() < limit) {
                byte b = buffer.get();
                switch (b) {
                    case '\r':
                        lastByte = b;
                        break;
                    case '\n':
                        if (lastByte == '\r') {
                            if (foundCrLf == true) {
                                return new ChunkHeader(0, buffer.position() - bufferOriginalPosition);
                            }
                            foundCrLf = true;
                        }
                        break;
                    default:
                        foundCrLf = false;
                        lastByte = b;
                }
            }
            return new ChunkHeader(-1, -1);
        }

        // Chunk header parsed. Youhou!
        return new ChunkHeader(chunkLength, chunkLength + (buffer.position() - bufferOriginalPosition));
    }
}
